//
// 和 CocosCreator 相关的公用方法
// 

import { Animation, bezier, Component, Node, Tween, tween, TweenEasing, UIOpacity, UITransform, Vec3 } from "cc";


export class CocosUtil {

    private constructor() { }

    /**
     * 使用 comp 的定时器创建一个 promise 用于 await 异步操作
     * @param delay 延时时长，单位：s 
     * @param comp 提供定时器的组件
     * @returns 
     */
    static waitByComp(delay: number, comp: Component): Promise<number> {
        delay = Math.max(0, delay);
        return new Promise<number>(resolve => {
            comp.scheduleOnce(resolve, delay);
        });
    }

    /**
     * 一个节点坐标系中的坐标转化为另一个节点坐标系中的坐标
     * @param fromNode 源节点坐标系
     * @param toNode 目标节点坐标系
     * @param out 转化后的坐标
     * @param fromPos 源节点坐标系中的坐标
     * @returns 
     */
    static posNode2Node(fromNode: Node, toNode: Node, out?: Vec3, fromPos?: Vec3): Vec3 {
        out ??= new Vec3();
        fromPos ??= Vec3.ZERO;
        const fromUITrans = fromNode.getComponent(UITransform) ?? fromNode.addComponent(UITransform);
        fromUITrans.convertToWorldSpaceAR(fromPos, out);
        const toUITrans = toNode.getComponent(UITransform) ?? toNode.addComponent(UITransform);
        toUITrans.convertToNodeSpaceAR(out, out);
        return out;
    }


    /**
     * 将世界坐标转换为节点坐标
     * @param wPos 世界坐标
     * @param targetNd 目标节点
     * @returns 
     */
    static convertWorld2SpaceAR(wPos: Vec3, targetNd: Node): Vec3 {
        let uiTrans = targetNd.getComponent(UITransform) ?? targetNd.addComponent(UITransform);
        return uiTrans.convertToNodeSpaceAR(wPos, new Vec3());
    }

    /**
     * 更换父节点，保持位置不变
     * @param node 节点
     * @param parent 更换后的父节点
     */
    static changeParent(node: Node, parent: Node) {
        if (node.parent === parent) {
            // console.log('changeParent 父节点未改变');
            return;
        } else if (!parent.isValid) {
            // console.log('changeParent 父节点已销毁');
            return;
        }
        const worldPos = node.getWorldPosition();
        const worldScale = node.getWorldScale();

        node.parent = parent;
        node.setWorldPosition(worldPos);
        node.setWorldScale(worldScale);
    }

    /**
     * 修改节点的透明度
     * @param node 
     * @param targetOpacity 目标透明度
     * @param duration 渐变耗时，默认=0。<=0 时表示无渐变动画，
     */
    static changeOpacity(node: Node, targetOpacity: number, duration: number = 0) {
        const uiOpacity = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity)!;
        Tween.stopAllByTarget(uiOpacity);
        if (duration > 0) {
            tween(uiOpacity)
                .to(duration, { opacity: targetOpacity })
                .start();
        } else {
            // 无渐变动画
            uiOpacity.opacity = targetOpacity;
        }
    }

    /**
     * 修改节点的透明度 返回Promise, 可通过 promise.then() 执行下一步业务逻辑
     * @param node 
     * @param targetOpacity 目标透明度
     * @param duration 渐变耗时，默认=0。<=0 时表示无渐变动画，
     * @returns
     */
    static changeOpacityAndThen(node: Node, targetOpacity: number, duration: number = 0): Promise<void> {
        return new Promise<void>(resolve => {
            const uiOpacity = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity)!;
            Tween.stopAllByTarget(uiOpacity);
            if (duration > 0) {
                tween(uiOpacity)
                    .to(duration, { opacity: targetOpacity })
                    .call(resolve)
                    .start();
            } else {
                // 无渐变动画
                uiOpacity.opacity = targetOpacity;
                resolve();
            }
        });
    }

    /**
     * 按计划列表播放动画队列 可定制动画播放的流程  ※注意:播放过程中一旦被外部执行play 、stop或 pause 等API 打断了逻辑， 就会自动中止
     * @param ani 动画实例
     * @param planList 计划列表  {clipName动画剪辑名称, loops播放次数, duration播放时长(秒,可选参数), onPlay当前剪辑播放开始时的回调(可选参数), onFinished当前剪辑播放结束时的回调(可选参数), thisObj传入回调的this引用(可选参数)}[]    播放次数小于或等于0时, 将永久循环该动作 或者 达到指定播放时长后结束
     * 
     * @example
     * 例如 CocosUtil.animationPlayByPlanList(roleAni, [{clipName:"attack", loops:3}, { clipName:"skill", loops:1}, { clipName:"run", loops:0, duration:10}, { clipName:"idle", loops:0} ]).then(()=>{...})  角色动画播放3次攻击动作, 然后播放1次技能动作, 然后不限次数播放奔跑动作10秒, 然后不限次数不限时长进入待机状态同时在.then()里进行所有播放计划完成时的回调
     * 实用场景:大奖画面的出现动画->待机显示动画(同时回调数字变化效果)->退场动画 的连贯播放与结束回调
     * CocosUtil.animationPlayByPlanList(bigWinAni, 
     *      [
     *          { clipName:"enter", loops:1 }, 
     *          { clipName: "idle", loops 0, duration:5, onPlay:(clipName) => {console.log("展示大奖数字变化动画") }}, 
     *          { clipName:"exit", loops:1}
     *      ]
     * ).then(()=>{ console.log("所有大奖动画播放完毕") })
     * 
     */
    static async animationPlayByPlanList(ani: Animation, planList: { clipName: string, loops: number, duration?: number, onPlay?: (clipName?: string) => any, onFinished?: (clipName?: string) => any, thisObj?: any }[]): Promise<void> {
        return new Promise<void>(resolve => {
            const planListCopy = planList.concat();//为了保证 planList 可复用, 这里创建个列表副本

            var plan!: any;
            var discontinueTimes = 0;

            function onLastFrame() {
                discontinueTimes = 0;//重置discontinueTimes
                plan?.onFinished?.call(plan?.thisObj || null, plan?.clipName);
                discontinue();
                plan = null;
                if (discontinueTimes == 2) return;//连续2次触发discontinue, 即视为终止计划队列行为
                if (planListCopy.length > 0) {
                    nextPlan();
                }
                else {
                    resolve();//所有动作完成时回调 注意: 如果计划列表里某个动作的loops次数是0(无限), 那么这里永远不会被执行
                }
            }

            function discontinue() {
                discontinueTimes++;
                if (discontinueTimes == 2) return;
                ani.off(Animation.EventType.FINISHED, onLastFrame, ani);
                //ani.off(Animation.EventType.STOP, discontinue, ani);//stop()是直接打断, 不需要处理
                ani.off(Animation.EventType.PAUSE, discontinue, ani);
                ani.off(Animation.EventType.PLAY, discontinue, ani);
                ani.unschedule(onLastFrame);
            }

            function nextPlan() {
                plan = planListCopy.shift();
                let clipName = plan!.clipName;
                let loops = plan!.loops;
                let duration = plan!.duration;

                if (duration !== undefined) {
                    if (isNaN(duration) || duration < 0) {
                        duration = 0;
                    }
                }

                if (loops <= 0) {
                    loops = Infinity; // cocos 底层Animation无限循环时的repeatCount就是Infinity这个值
                }

                ani.getState(clipName)!.repeatCount = loops;
                ani.getState(clipName)!.setTime(0);
                ani.getState(clipName)!.update(0);
                ani.play(clipName);
                ani.scheduleOnce(() => {
                    ani.once(Animation.EventType.PLAY, discontinue, ani);//当开发者手动控制动画播放的时候, 终止计划队列
                    //ani.once(Animation.EventType.STOP, discontinue, ani);//stop()是直接打断, 不需要处理
                    ani.once(Animation.EventType.PAUSE, discontinue, ani);
                })

                plan?.onPlay?.call(plan?.thisObj || null, clipName);

                if (plan!.loops > 0) {//注意: 如果计划列表里某个动作的loops次数是0(无限) 并且duration === undefined, 那么下个动作永远不会被执行  所以不建议把除了最后一个动作以外的其他动作loops设为0
                    ani.scheduleOnce(() => {
                        ani.once(Animation.EventType.FINISHED, onLastFrame, ani);
                    }, 0);

                    if (duration !== undefined) {
                        ani.scheduleOnce(onLastFrame, duration);
                    }
                }
                else {
                    if (planListCopy.length === 0 && duration === undefined) {//如果最后一个动作是无限循环播放并且不限时长, 直接当做已经完成播放计划列表 并回调resolve()
                        discontinue();
                        resolve();
                    }
                    else if (planListCopy.length !== 0 && duration === undefined) {
                        console.warn("播放计划列表尚未结束, 但目前已经出现了无限次循环的剪辑: " + clipName + "  将无法执行剩余的播放计划!");
                    }
                    else if (duration !== undefined) {
                        ani.scheduleOnce(onLastFrame, duration);
                    }
                }
            }

            nextPlan();
        })
    }


    /**
     * 停止播放帧动画，并且状态停留在第 0 帧
     * @param ani 
     * @param aniName 
     */
    static stopAnimation(ani: Animation, aniName: string) {
        const aniState = ani.getState(aniName);
        if (aniState) {
            aniState.setTime(0);
            aniState.update(0);
        }
        ani.stop();
    }

    /**
     * 刷新需要本地化的组件，自动根据 l10nKey 获取对应的多语言文本或资源来刷新组件显示。
     * 注意：因为游戏内不支持切换语言，所以该方法只需要在周期函数 onLoad 中调用一次即可。
     * @param node 包含所有本地化组件的节点，一般为某个组件的根节点。
     * @param l10nCompMap Map<挂载组件的节点名, 需要本地化的组件的类型和 l10nKey>
     *      l10nKey 多语言文本或资源的 key，即 L10nTxtCfg、L10nResCfg 的 key。
     *      compType 本地化组件的类型，如 cc.Sprite、cc.Label。（目前只有 Sprite 和 Label 两种）
     */
    // static updateL10nComps(node: Node, l10nCompMap: Map<string, { l10nKey: string, compType: typeof Sprite | typeof Label }>) {
    //     const compTypeMap = new Map<string, TCompConstructor<Component>>();
    //     l10nCompMap.forEach((l10nComp, compName) => {
    //         compTypeMap.set(compName, l10nComp.compType);
    //     });
    //     const compMap = node.findCompMap(compTypeMap);
    //     compMap.forEach((comp, compName) => {
    //         const { l10nKey, compType } = l10nCompMap.get(compName)!;
    //         if (compType === Sprite) {
    //             L10nMgr.instance.getRes(l10nKey).then(l10nRes => {
    //                 if (!l10nRes) return;
    //                 (comp as Sprite).spriteFrame = l10nRes as SpriteFrame;
    //             });
    //         } else if (compType === Label) {
    //             const l10nStr = L10nMgr.instance.getStr(l10nKey);
    //             (comp as Label).string = l10nStr;
    //         }
    //     });
    // }

    // static updateL10nGameLanguage(args: LanguageArgs[]) {
    //     if (args.length == 0) return;
    //     args.forEach((v) => {
    //         const compType = v.compType
    //         if (compType == LanguageCompType.Sprite) {
    //             L10nMgr.instance.getRes(v.L10nTxtCfg).then(l10nRes => {
    //                 if (!l10nRes) return
    //                 v.languageSpr.spriteFrame = l10nRes as SpriteFrame
    //             })
    //         }else if (compType == LanguageCompType.Label) {
    //             const l10nStr = L10nMgr.instance.getStr(v.L10nTxtCfg)
    //             v.languageLbl.string = l10nStr
    //         }
    //     })
    // }

    /**
  * 贝塞尔曲线运动到指定位置
  * @param targetNd 移动目标节点
  * @param playTime 移动时间
  * @param startPos 起始位置
  * @param midPos 中间位置
  * @param endPos 最终位置
  * @param easing 缓动
  */
    static playBezierTween(targetNd: Node, playTime: number, startPos: Vec3, midPos: Vec3, endPos: Vec3, easing: TweenEasing | undefined | ((k: number) => number), callBack?: Function) {
        let ft = tween(startPos);
        const mixY = midPos.y;
        const maxY = endPos.y;
        const mixX = midPos.x;
        const maxX = endPos.x;
        let progressX = function (start: number, end: number, current: number, t: number) {
            current = bezier(start, mixX, maxX, end, t);
            return current;
        }
        let progressY = function (start: number, end: number, current: number, t: number) {
            current = bezier(start, mixY, maxY, end, t);
            return current;
        }

        ft.parallel(
            tween()
                .to(playTime, { x: endPos.x }, {
                    progress: progressX,
                    easing: easing,
                    onUpdate: (target, ratio) => {
                        targetNd.setPosition(startPos);
                    }
                }),
            tween()
                .to(playTime, { y: endPos.y }, {
                    progress: progressY,
                    easing: easing,
                    onUpdate: (target, ratio) => {
                        targetNd.setPosition(startPos);
                    }
                })
        ).call(() => {
            if (callBack) {
                callBack()
            }
        })
            .start();
    }



}
